(ns util.cmd
  (:use util.cmd.rc)
  (:require clojure.string))

(defn- keyword->arg [arg]
  (cond
    (keyword? arg) (let [arg (name arg)]
                     (cond
                       (re-matches #"^[^-]$" arg) (str "-" arg)
                       (re-matches #"^[^-].+$" arg) (str "--" arg)
                       :else arg))
    (instance? StringBuilder arg) arg ; XXX: dirty trick...
    :else (str arg)))

(defn- transform-cmdspec [specs]
  (vec
    (for [spec specs]
      (if (and (vector? spec) (= (count (drop-while symbol? spec)) 2))
        (conj (vec (for [x (butlast spec)] `'~x)) (last spec))
        `'~spec))))

(defn- make-map [args cmdspec defaults]
  (let [{spec true [rest-sym] false} (group-by vector? cmdspec)
        rest-str (str rest-sym)
        key-data (into {} (for [[syms [_ default]] (map #(split-with symbol? %)
                                                        (conj spec '[help? h?]))
                                :let [sym-keywords (map #(keyword (re-find #"^.*[^?*]" (str %))) syms)
                                      defaults (vals (select-keys defaults sym-keywords))
                                      default (if (seq defaults) (first defaults) default)]
                                sym syms]
                            [(re-find #"^.*[^?*]" (str sym))
                             {:sym (str (first syms)) :default default}]))
        key-data (merge key-data
                        (into {} (for [[k v] (filter #(.endsWith (:sym (second %)) "?") key-data)]
                                   [(str "no-" k) (assoc v :! true)])))
        defaults (into {} (for [[_ {:keys [default sym]}] key-data
                                :when default]
                            [sym default]))]
    (loop [[argkey & [argval :as r]] (map keyword->arg args)
           cmdmap (assoc defaults :cmdspec cmdspec rest-str [])]
      (if argkey
        (let [[_ keybase inline-value] (re-find #"^--?(.*?)(?:=(.*))?$" argkey)]
          (cond
            (= keybase nil) (recur r (update-in cmdmap [rest-str] conj argkey))
            (= keybase "")  (update-in cmdmap [rest-str] #(apply conj % r))
            :else (if-let [found (key-data keybase)]
                    (cond
                      (:! found) (recur r (assoc cmdmap (:sym found) false))
                      (= \? (last (:sym found))) (recur r (assoc cmdmap (:sym found) true))
                      (and (= \* (last (:sym found))) inline-value) (recur r (assoc cmdmap (:sym found)
                                                                               (conj (get cmdmap (:sym found) [])
                                                                                 inline-value)))
                      (= \* (last (:sym found))) (recur (next r) (assoc cmdmap (:sym found)
                                                                   (conj (get cmdmap (:sym found) [])
                                                                     (if (or (nil? r) (= \- (ffirst r)))
                                                                         (:default found)
                                                                         (first r)))))
                      inline-value (recur r (assoc cmdmap (:sym found) inline-value))
                      :else (recur (next r) (assoc cmdmap (:sym found)
                                              (if (or (nil? r) (and (not= (first r) "-") (= \- (ffirst r))))
                                                (:default found)
                                                argval))))
                    (throw (Exception. (str "Unknown option " argkey))))))
        cmdmap))))

(defn- align
   "Align strings given as vectors of columns, with first vector
   specifying right or left alignment (:r or :l) for each column."
   [spec & rows]
   (let [maxes (vec (for [n (range (count (first rows)))]
                        (apply max (map (comp count #(nth % n)) rows))))
         fmt (clojure.string/join " "
                  (for [n (range (count maxes))]
                     (str "%"
                        (when-not (zero? (maxes n))
                           (str (when (= (spec n) :l) "-") (maxes n)))
                          "s")))]
      (clojure.string/join "\n"
         (for [row rows]
            (apply format fmt row)))))

(defn- rmv-q
   "Remove ?"
   [#^String s]
   (if (or (.endsWith s "?") (.endsWith s "*"))
      (.substring s 0 (dec (count s)))
      s))

(defn- desc-to-options [options]
  (if (string? options)
    {:description options}
    options))

(defn- print-help [desc cmdmap]
  (println desc)
  (println "Options")
  (println
     (apply align [:l :l :l]
        (for [spec (:cmdspec cmdmap) :when (vector? spec)]
            (let [[argnames [text default]] (split-with symbol? spec)
                  [_ opt q] (re-find #"^(.*[^?])([?*]?)$"
                                 (str (first argnames)))
                  argnames  (map (comp rmv-q str) argnames)
                  argnames
                        (clojure.string/join ", "
                          (for [arg argnames]
                            (if (= 1 (count arg))
                              (str "-" arg)
                              (str "--" arg))))]
               [(str "  " argnames (when (= "" q) " <arg>") " ")
                text
                (if-not default
                  ""
                  (str " [default " default "]"))])))))

(defn run-command-line [options args cmdspec f]
  (let [options (desc-to-options options)
        rc (-> options :defaults :rc)
        defaults (when rc (load-rc rc))
        cmdmap (make-map args cmdspec defaults)]
    (if (cmdmap "help?")
      (print-help (:description options) cmdmap)
      (f cmdmap))))

(defmacro with-command-line
  "Bind locals to command-line args."
  [args options cmdspec & body]
  (let [locals (vec (for [spec cmdspec]
                      (if (vector? spec)
                        (first spec)
                        spec)))]
    `(run-command-line
      ~options
      ~args
       ~(transform-cmdspec cmdspec)
       (fn [{:strs ~locals}]
         ~@body))))

